【Golang零基础入门】06 Goroutine

引言

现在很多一线大厂都转型到Go,究其原因就是该语言对并发支持的好。
相信大家都知道Golang是在语言级原生支持协程,先看看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
func main()  {
for i:=0; i<10; i++ {
go func(i int) {
for {
fmt.Printf("协程: %d \n", i)
}
}(i)
}
// 防止程序来不及打印就退出
time.Sleep(10*time.Microsecond)
}

输出结果如下,你会发现每次运行,输出内容都是不一样的,这是为什么呢?

一、并行与并发

并行是指程序的运行状态,要有两个线程正在执行才能算是Parallelism;

并发指程序的逻辑结构,Concurrency则只要有两个以上线程还在执行过程中即可。

简单地说,Parallelism要在多核或者多处理器情况下才能做到,而Concurrency则不需要。
对应后面的runtime.GOMAXPROCS(runtime.NumCPU())

二、进程,线程与协程

goroutines是Go的并发基础,为了帮助我们理解这个概念,我们先来谈一谈并发的发展故事。

2.1 进程 Process

进程是程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是线程的容器。

在分时系统中,操作系统通过记录当前进程的状态,然后恢复另一个进程的状态,在活动进程之间快速切换CPU的处理,而产生了一种并发执行的错觉,这个过程称为上下文切换(switch cost)。

上下文切换的成本是:

  • 内核需要存储该进程的所有CPU寄存器的内容,然后恢复另一个进程的值。由于进程切换可以在进程执行的任何时刻发生,操作系统需要存储所有这些寄存器的内容,因为它不知道当前正在使用哪些寄存器。
  • 内核需要将CPU的虚拟地址刷新为物理地址映射(TLB缓存)。
  • 操作系统上下文切换的开销,以及选择下一个进程都会占用CPU的调度程序函数的开销。

2.2 线程 thread

线程是操作系统能够进行运算调度的最小单位,在概念上与进程类似,但共享相同的内存空间。由于线程共享地址空间,因此创建、切换速度更快。但仍然有昂贵的上下文切换成本; 必须保留很多状态。

2.3 协程 Coroutine

协程是用户态线程,不依靠内核来管理调度,具备以下特点:

  • 轻量级线程
  • 非抢占式多任务处理,由协程自己本省主动交出控制权

以下列出了goroutine之间切换的主要的时间点:

(1)Channel发送和接收操作(如果这些操作是阻塞的);
(2)执行go语句,虽然不能保证新的goroutine马上被调度执行;
(3)阻塞的系统调用,像文件操作,网络操作,IO等等;
(4)停下来进入垃圾回收周期以后。

换句话讲,在goroutine不能继续进行运算以后(需要更多数据,更多空间,等等),都会进行切换。


双向通道就是channel

GO的协程 Goroutines

每个并发的执行单元叫做goroutine,用关键词go来创建。
具体goroutine放在哪个线程,由调度器控制与切换。

假设要计算两个非常复杂的逻辑,然后输出结果。在正常是线性程序里面,会依次调用计算的逻辑,完成之后再调用输出逻辑。但如果是在有两个甚至更多个goroutine的程序中,对两个计算逻辑的调用就可以在同一时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
for i:=0; i<100; i++ {
defer wg.Done()
fmt.Println("A:", i)
time.Sleep(1*time.Second)
}
}()

go func() {
for i:=0; i<100; i++ {
defer wg.Done()
fmt.Println("B:", i)
time.Sleep(2*time.Second)
}
}()
wg.Wait()
}

我们运行这个程序,会发现A和B前缀会交叉出现,即两个程序是并发在执行的,并且每次运行的结果可能不一样,这就是Go调度器调度的结果。

这里的sync.WaitGroup其实是一个计数的信号量,使用它的目的是要main函数等待两个goroutine执行完成后再结束,不然这两个goroutine还在运行的时候,程序就结束了,看不到想要的结果。

sync.WaitGroup的使用也非常简单,先是使用Add 方法设设置计算器为2,每一个goroutine的函数执行完之后,就调用Done方法减1。Wait方法的意思是如果计数器大于0,就会阻塞,所以main 函数会一直等待2个goroutine完成后,再结束。

对于逻辑处理器的个数,不是越多越好,要根据电脑的实际物理核数,如果不是多核的,设置再多的逻辑处理器个数也没用,如果需要设置的话,一般我们采用如下代码设置。

1
runtime.GOMAXPROCS(runtime.NumCPU())

所以对于并发来说,就是Go语言本身自己实现的调度,对于并行来说,是和运行的电脑的物理处理器的核数有关的,多核就可以并行并发,单核只能并发了。

本文标题:【Golang零基础入门】06 Goroutine

文章作者:Craze lee

发布时间:2019年04月20日 - 11:04

最后更新:2019年05月20日 - 16:05

原始链接:http://craze-lee.github.io/2019/04/20/Golang/零基础入门/06 Goroutine/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

您的支持将鼓励我继续创作!